## Режимы доступа public, private, protected. Сеттеры и геттеры

Python не предоставляет никакой системы строгой инкапсуляции, которая присуща некоторым языкам программирования. 

Из-за этого вы можете получить доступ к любому атрибуту или методу класса из внешнего кода. 

Однако существует общепринятое соглашение между разработчиками, что атрибуты и методы, предназначенные для внутреннего использования, должны начинаться с подчеркивания (например, `_visible`). 

Это подчеркивание служит индикатором для других разработчиков, что этот атрибут или метод не должен изменяться напрямую извне. 

Но всегда следует помнить, что `Python` не предоставляет никаких средств для принудительного соблюдения этого соглашения, и атрибуты с подчеркиванием по-прежнему доступны извне.


## Закрепление пройденного. Переопределение методов

### Пример с лампочками

Создадим класс Lamp как светильник

```python
class Lamp:
    pass

lamp1 = Lamp()
lamp2 = Lamp()

print(isinstance(lamp1, Lamp))
print(isinstance(lamp2, Lamp))
```

Результат:

> True

Лампочки уже есть, но они ничего не могут делать. Зачем они тогда нужны.

Вспомним концепцию ООП – все лампочки общаются сообщениями.

Или другой вариант – у объектов есть поведение.

Человек может ходить, автомобиль ехать. Лампочка включаться-выключаться.

Человек общается с человеком передачей сообщений. Для этой цели в ООП используются методы.

По сути класс – это набор методов.

А методы – ничто иное как обычные функции, просто которые описаны в классе. Все-что мы знаем о функциях здесь легко применить.

Добавим функции включения-выключение лампочки

```python
class Lamp:
    def switch_on():
        print('Лампочку включили')

    def switch_off():
        print('Лампочку выключили')

```

и пошлем сообщение “Лампочка включись”

```
lamp1.switch_on()
lamp1.switch_off()
```

но мы видим ошибку

> TypeError: Lamp.switch_on() takes 0 positional arguments but 1 was given

Ошибка типа: `Lamp.switch_on()` принимает 0 позиционных аргументов, но был задан 1.

Где это параметр, я ничего не передал.

Используем принцип наименьшего удивления

В чем причина? У нас два объекта, а метод у них один.

А интерпретатор внутри класса не понимает у какого объекта нужно дернуть метод `switch-on/off`.

---

### Конструктор класса

Проблема в области видимости.

В глобальной мы всё видим, а класс внутри себя не понимает кто в него стучится.

Поэтому Пайтон всегда неявно сюда первым параметром передает этот объект
`lamp1.switch_on(lamp1)`

а метод switch должен принять его

```python
def switch_on(себяшка):
    print('Лампочку включили')
```

т.е. себяшка – обычный аргумент функции.

Но по конвенции это дело обычно называют `self`.

Возникает вопрос – зачем этот `self`, если он передается во все методы, зачем его передавать, зачем его указывать.

Но увы, конвенция, и во всех методах мы обязаны его указывать. 

Но с другой стороны.

В `php` и `JS` таким словом является `this`.

Но причем там оно явно не пишется

Во внешнем мире он `lamp` или `Александр Петрович`, а в духовном мире (волшебно-магическом) `отец Георгий` или `self`.

Итак перед вами – минимальная концепция ООП, ОО-парадигмы.

Открой, закрой, включи.

Обычные функции. Общий принцип

```python
    def имя_метода (self):
        pass

объект = Lamp()
объект.имя_метода()
```

Технически на этом можно остановиться, но у нас есть засада.

Лампочку можно выключать бесконечное множество раз.

А каким образом выключать то, что еще не включили.

Значит нам нужен флаг или логическое состоянии.

Т.е. кроме поведения, к примеру у человека – бегать, прыгать, есть, говорить, учится, у человека есть состояния – возраст, имя,
Соответственно у объектов тоже есть `состоянии`.

И они называются `атрибутами`.

Атрибут вроде как обычная переменная.

Добавим атрибут состояния в класс Lamp

```python
class Lamp:
    def switch_on(self):
        self.state = True
        print('Лампочку включили')

    def switch_off(self):
        if self.state == True:
            print('Лампочку выключили')
            self.state == False


lamp1.switch_on()
lamp1.switch_off()
```

работает!? Но!

```python
lamp1.switch_on()
lamp1.switch_off()

lamp1.switch_off()
```

или просто выключить без включения

```python
lamp1.switch_off()
```

> AttributeError: 'Lamp' object has no attribute 'state'

Нет атрибута state у лампы.

Мы можем конечно, что то здесь проверит в `switch`, поставить `if` и так далее

Но на самом деле, правильный путь – где-то это состояние проинициализировать.

Инициализацией кода в ООП называется конструктор.

---

### Конструктор

Конструктор – специальные метод, который (1) запускается автоматически, (2) запускается при создании объекта

Служебное имя у конструктора `__init__`

Иногда можно встретить вместо `конструктор` слова `инициализатор`, а даже `инстанс` (в java).

Т.о. создается объект, вызывается его конструктор, записывается в память (как правило это обычная переменная).

Это обычная переменная, все объекты в пайтоне – ссылочного типа.

Здравствуй, волшебство. Привет, дандер-методы.

Эти методы четко определены с помощью двойного подчеркивания до и после имени метода
Как правило все атрибуты создаются, инициализируются в конструкторе

```python
    def __init__(self):
        self.state = False
```

т.е по дефолту наша лампочка еще не горит.
Теперь мы можем по-человечески отследить состояние лампочки

```python
    def switch_on(self):
        if not self.state:
            self.state = True
            print('Лампочку включили')

    def switch_off(self):
        if self.state:
            print('Лампочку выключили')
            self.state = False
```

Поэкспериментируем в консоли.

Логика не нарушена, дизайн хороший. Багов и дыр нету?

Краткий итог – `self, init`

Появление переменных где-то в середине кода – плохой стиль, откуда, чего, куда, зачем.

Поэтому и метод `init` помогает решить эту задачу.

Однако, вдруг нам захочется определить еще какой-нибудь атрибут у лампочки, например этажность

```python
self.floor = 1
```

Но так явно не совсем удобно, допустим мне бы хотелось явно при создании лампочки, указать этаж, на котором её разместить

```python
lamp1 = Lamp(1)
lamp2 = Lamp(2)
```

поэтому здесь можно определить стандартный параметр

```python
  def __init__(self, floor=0):
        self.state = False
        self.floor = floor
```

Таким образом, мы можем напрямую обращаться к его свойствам, атрибутам.

```python
lamp1.state
```

> False
> lamp2.floor
> 2

Однако, если я захочу вывести

```python
print (lamp2)
```

результат будет следующий

> `<**main**.Lamp object at 0x0000025210B5D250>`

Но это не очень информативно, но такую возможность упускать не нужно.

Но нам нужно, чтобы при обращении к лампе команда `print` вывела состояние лампочки, на каком она этаже, сколько её ресурс и т.п.

И первый раз мы подходим к понятию переопределение методов.

У пайтона есть много служебных методов, и нашем случае мы обращались к методу `repr`.

От слова representation

```python
    def __repr__(self):
        return f'Я лампочка на {self.floor} этаже'
```

на самом деле здесь 2 таких метода, второй метод `__str__`

и он вызывается только в принте, `repr` во всех случаях.

```python
def __str__(self):
      return f'Я лампочка на {self.floor} этаже'
s = lamp2
print (s)
```

волшебный дандер метод `__repr__` более универсален

Т.о. класс набор методов, которые описывают поведение будущего объекта (экземпляра будущего класса).

Также у объекта могут быть атрибуты, состояния.

В некоторых книжках очень много акцентов делается на атрибуты.

Но атрибуты вторичны.

Концепция ООП крутится вокруг методов.

Класс – несколько функций во одном флаконе, коробка в которой хранятся методы и переменные.

Можно легко накосячить и уйти в некуда.

---

## Погружение в инкапсуляцию

### Данные экземпляра, методы экземпляра и свойства экземпляра

Начнем знакомство с принципом № 1 в ООП – «Инкапсуляция».

У лампочки есть состояние, есть выключатель, и мы не залезаем внутрь выключателя, чтобы понять как он работает.

Не нужно быть сам-себе режиссером – я сам пишу класс и сам его использую.

НЕТ!

Класс пишет один человек, а использует его другой. 

И тот, который использует этот класс, не должен залезать внутрь класса (выключатель эти или телевизор), чтобы понять, что там внутри.

Представьте он на замок закрыт.

Нам не должно быть интересно, как работает коробка передач автомобиля.

Не лезь – сломаешь

Типичный пример обращения напрямую к состоянию лампочки

```python
lamp1.state
False
lamp1.state = True
lamp1.state
```

Используя магию `__getattribute__`, мы можем просто запретить прямо обращение к атрибутами

```python
    def __getattribute__(self, item):
        # print('Прямой доступ')
        # raise ('Прямой доступ')
        return object.__getattribute__(self, item)

    def __setattr__(self, key, value):
        print('Нет доступа')
        object.__setattr__(self, key, value)

print(lamp1.state)
lamp1.state=None
print(lamp1.state)

```

Но согласно дзена обращение к атрибутам  делается по-другому  

По порядку.

Мы дали напрямую обратиться к атрибуту экземпляра класса.

Мы не должны были этого делать – давать доступ напрямую.

В джаве есть поля и свойства (вместо атрибутов).

У нас в питоне атрибуты и свойства.

Атрибуты – непосредственно хранят эти свойства.

А свойства – это интерфейс доступа к этим значениям.

Как сделать интерфейс доступа к этим значениям.

Как это делается в других языках?

Например в Джаве к каждому полю пишется свои методы `get state` и `set state`.

```python
def get_floor(self):
    return self.floor

def set_state(self, f):
    self.state = f
```

Результат

```python
lamp2.get_floor()
2
lamp2.set_floor(4)
lamp2.get_floor()
```

а если я снова сделаю `lamp1.floor` я снова получу доступ напрямую к `floor`.

Для этих целей атрибут можно сделать приватным.

Для этого использую ключевые слова, которых в пайтоне нет.

`Private, protected и public`.

А в пайтоне – первое это конвенция, просто начать имя атрибута с подчеркивания.

```python
self._floor = floor
```

это чисто указатель, внутренняя кухня, конвенция.

Если мы хотим действительно сделать свойство закрытым от внешнего мира – физически невозможный доступ к атрибуту, название атрибута нужно начать с двух подчеркиваний. 

И здесь уже конкретная приватность.

Теперь достучаться до `floor` из внешнего мира невозможно.

Из глобальной видимости его не видно.

Теперь мы можем к нему обратиться с помощью методов … 

Однако это не удобно, каждый раз обращаться к функциям.

Поэтому в Пайтоне применяется трюк следующего содержания. Есть два способа, два варианта.

Делаем `__state` приватным.

Пишем метод `get_state`. 

По задумке получать состояние можно, изменять состояние нет.

```python
def get_state(self):
    return self.__state
```

и далее пишем переменную `state` (вне метода) и с помощью специальной функции property передаю функцию `get_state` которая должна выступать в роли геттера.

```python
state = property(get_state)
```

Теперь можно получить состояние лампочки

```python
print (lamp1.state)
```

а поменять состояние нельзя

```python
lamp1.state = True
```

как это работает?

При обращении к `lamp1.state` в области видимости класса `Lamp` ищется метод с названием `lamp` и не находится, но находится переменная `state` которая через `property` вызывает функция `get_state`, которая возвращает состояние лампы.

Это не что иное, как интерфейс доступа.

Т.е. атрибуте `state` имеет атрибут доступа.

Но пока у него есть возможность только получить данные.

А теперь тоже самое сделаем с этажностью.

Для этого приватим `floor`.

Пишем свойство floor, обращаемся к функции property, внутри которой передаем сначала геттер, потом сеттер.

Теперь мы к нашей этажности имеем доступ как на чтение, так и на запись.

```python
def get_floor(self):
        return self.__floor

def set_floor(self, f):
    self.__floor = f

floor = property(get_floor, set_floor)
```

Результат:

```python
lamp2.floor
2
lamp2.floor=4
lamp2.floor
```

У вас должен возникнуть вопрос.

В чем проблема, в чем задача, связанная с этим гетом и сетом.

Зачем я сделал `state` приватным.

По правилам ООП напрямую обращаться к методам нельзя.

В данном случае читать можно, изменять нельзя.

Зачем я сделал это с этажом и наградил его сеттером.

Ответ и причина кроется в обратной совместимости.

Когда мы ему даем прямой доступ, код от нас идет куда-туда, минуя нас, мы не можем перехватить, что посылается в атрибут состояния или в атрибут этажность.

А если туда полетит какая-нибудь абракадабра.

А мы должны данные перехватывать, перед тем как они туда запишутся или уйдут оттуда.
Представьте себе, что-то сломалось или тз поменялось.

Допустим этажность нужно писать русскими буквами

```python
lamp1 = Lamp('один')
lamp2 = Lamp('ресепшен')
```

И тут мы получаем проблему, что новый код, должен писать кириллицу, а старый работает только с целочисленными этажами.

А вдруг согласно ТЗ всё нужно писать в верхнем регистре.

Нам нужен в сеттере преобразователь.

Т.е. перед тем как записать и отдать нам нужен какой-то код.

Пример из жизни – Я хочу узнать как вас зовут.

Я должен вам послать сообщение или спросить – как Вас зовут.

Если я к вам обратился – вы ответите `Иван Иванович`, а если пацан на улице – вы ему `Вано` скажете, или там отправите его в `Монино`.

Т.е. если я очень хочу узнать ваше имя, я это не смогу сделать.

Не давайте доступа без нужды. 

Не пишите на одежде как вас зовут.

Подводим итог по первому принципу ООП – инкапсуляция.

В Пайтоне она реализована через `_` и `__` подчеркивания, а также функции property.


Есть еще одна возможность реклизовать скрытие(инкапсулировать).

 Использовать декораторы для свойств.

```python
 @property
    def state(self):
        return self.__state

    @state.setter
    def state(self, value):
        self.__state = value

    # def get_floor(self):
    #     return self.__floor
    #
    # def set_floor(self, f):
    #     self.__floor = f
    #
    # floor = property(get_floor, set_floor)
```

Они используются вот таким образом.

Предположим, что у меня есть атрибут `private` и я хочу описать для него `геттер` и `сеттер`.

Геттер описывается через декоратор property и описываем соответственно, метод с именем, который будет дергаться (вызываться).

Чтобы описать сеттер, декоратор становится именем свойства. `@state` (имя свойства) точка `setter`.
Таким образом мы имеем два метода с одинаковым именем.

Еще раз посмотрим на код и зафиксируем синтаксис инкапсуляции в Пайтон с помощью декоратора property и имя_свойства.setter.


Результат:

```python
print (lamp2)
lamp1.state = 6
print (lamp1.state)
lamp1.floor = 2
print (lamp2)
```

Это все встроенное в Пайтон.

Соглашения, конвенция, дзен, философия. Синтаксический сахар.

Однако, мы всё пока крутится вокруг объекта класса, т.е сущности которые класс порождает – лампочки, породы собак, модели автомобилей.

Но в пайтоне все является объектом и даже класс. Поэтому мы можем смело создать атрибуты у самого Класс.

Атрибут класса

```python
class ИмяКласса:
    атрибут_класса = значение_по_умолчанию

    def метод(self, параметр):
        ИмяКласса.атрибут_класса = параметр
```

Т.е. они указываются прямо в классе. В чем фича?

Атрибут общий для всех порождаемых классом сущностей.
У человека – две руки, у автомобиля – есть кузов, двигатель.

Или константа pi, e или число Авогадро.

Или у всех ламочек бренд `Филипс`

```python
class Lamp:
    brand = "Philips"
Если мы обращаемся извне, то мы пишем имя класса и атрибут:

объект = ИмяКласса()
объект.метод(значение)

print(ИмяКласса.атрибут_класса)

print(lamp2.brand)
```

а конструкторе (инициализаторе) прописать

```python
  def __init__(self, floor=0):
        self.__state = False
        self.__floor = floor
        print(f'Создана лампочка {Lamp.brand}' )
```

?Оно не принадлежит объекту? Оно унаследовано от …
Для чего еще могу быть использованы атрибуты класс. К примеру, для подсчета общего количества лампочек

```python
class Lamp:
    brand = "Philips"
    count = 0
    def __init__(self, floor=0):
        self.__state = False
        self.__floor = floor
        Lamp.count +=1
        print(f'Создана лампочка {Lamp.brand} № {Lamp.count}' )
```

Результат

```
Создана лампочка Philips № 1
Создана лампочка Philips № 2
Создана лампочка Philips № 3

print(f'Всего лампочек - {Lamp.count}')
Всего лампочек – 3

```

Это довольно удобно.

Кроме того, у объекта класса могут быть не только методы, но и методы уровня класса и статические методы. 

Не что иное как поведение класса.


Причем, у нас есть две возможности – статический и классический (класс) методы.

## Классные методы (методы класса)

```python
class ИмяКласса:
    атрибут_класса = 0
    @staticmethod
    def static_method():
        return ИмяКласса.атрибут_класса

    @classmethod
    def class_method(cls):
        return cls.атрибут_класса

объект = ИмяКласса()

объект.static_method()
объект.class_method()
```

Технический между ними разницы нет. 

Есть синтаксическая разница. Если мы используем декоратор `@staticmethod` то обращение идет к классу через точку его метод.

```python
    @staticmethod
    def static_method():
        return Lamp.count

    @classmethod
    def class_method(cls):
        return cls.count


print (lamp1.static_method())
print (lamp1.class_method())
```

Используя класс `Lamp` – мы отработали первый принцип ООП – инкапсуляцию.

Первый способ – через функцию `property`, второй способ через декоратор `property`. 

Мне удобнее через функцию `property`. 

Если используем класс-метод (в терминах ООП – это тоже классический метод).

Но в класс-методе, декоратор автоматом передает имя класс.

Некая переменная в которую прилетает имя класса, по соглашению, по конвенции – название её `cls` ( по аналогии `self`).

В первом случае имя класс мы пишем руками.

Никакого `self`. Но какая возникает проблема.

Подумайте, если мы поменяем имя класса. Что это значит?

Нам нужно будет поменять имя класса во всех статических методах.

В случае с класс-методами – этого делать не нужно.

Разница в терминах ООП никакой. Кроме того, что необходимо менять название в статик-методах.

Ну и, соответственно, внутри класса мы не можем обращаться к каким-либо объектам. Только к классу.

К общему поведению всех объектов.

Т.е класс – это тоже самостоятельная единица.

## P.S.

На самом деле, в Python можно относительно легко обратиться и к приватным атрибутам извне. Если распечатать все атрибуты экземпляра:

```python
print(dir(lamp1))
```
то среди прочих мы увидим, следующие:

`'_Lamp__floor', '_Lamp__state'`

Это и есть кодовые имена приватных атрибутов, к которым мы можем обратиться через ссылку `lamp1`:

```python
print(lamp1._Lamp__state)
```
и менять их. 

Однако, так делать крайне не рекомендуется и двойное подчеркивание должно сигнализировать программисту, что работать с такими атрибутами нужно только через разрешенные интерфейсные методы.

Иначе, возможны непредвиденные ошибки.

Если у вас появилась необходимость лучше защитить методы класса от доступа извне, то это можно сделать с помощью модуля `accessify`. Для его установки нужно выполнить команду:

```python
pip install accessify
```

И, затем, импортировать из него два декоратора:
```python
from accessify import private, protected
```

Далее, нужный декоратор просто применяем к методу и он становится либо приватным (private), либо защищенным (protected):
```python
    @private
    @classmethod
    def check_value(cls, x):
        return type(x) in (int, float)
```
Все, теперь мы можем обратиться к check_value только внутри класса, но не извне


Я, думаю, из этого занятия вам стало понятно, как реализуются режимы доступа `public, protected и private`, а также, как правильно обращаться к скрытым атрибутам через интерфейсные методы – сеттеры и геттеры.